package math.functions;

import graphics.Graphable2D;
import graphics.GraphingProperties;
import java.util.Arrays;

/**
 * <p>The Polynomial Class. A Class that allows basic operations with
 * Polynomials of arbitrary degrees.</p> 
 * 
 * <p><b> Internal Representation: </b></p>
 * <p> Internally, the Polynomials are stored as double arrays, where the 
 * first element in the array (index 0) is the constant of the polynomial. 
 * The second element (index 1) is the coefficient of the term with degree 1,
 * the third element (index 2) is the coefficient of the term with degree 2, 
 * etc. For example, if the Polynomial is: f(x) = 5.0 + 2.0x - 9.8x^2, then 
 * the double array would be: [5.0, 2.0, -9.8]. </p> 
 * 
 * <p><b> Operations on Polynomials </b></p>
 * <p> Note that basic operations dealing with Polynomials are included 
 * in this class (such as adding and multiplying Polynomials). When calling
 * on a method that performs operations on one or more Polynomials 
 * (such as add(Polynomial) or multiply(Polynomial)), the method does NOT 
 * modify those Polynomial Objects. Rather a new Polynomial Object storing
 * the result of those operations are returned. </p>
 * 
 * <p><b> Graphing Polynomials </b></p>
 * <p> Since this Class implements Graphable2D, any Polynomial Object can
 * be graphed using Class graphing.Graphable2D </p>
 * 
 * Fully Tested: Yes
 * Fully Documented: Yes
 * Created: 11-Dec-2011
 * Last Updated: 13-Dec-2011
 * 
 * @author Shimu Wu
 */
public class Polynomial implements Graphable2D {

    /**
     * The simplest polynomial function - a constant value; f(x) = 1
     */
    public static final Polynomial CONSTANT;

    /**
     * A simple linear function; f(x) = x
     */
    public static final Polynomial LINEAR;

    /**
     * A simple quadratic function; f(x) = x<sup>2</sup>
     */
    public static final Polynomial QUADRATIC;

    /**
     * A simple cubic function; f(x) = x<sup>3</sup>
     */
    public static final Polynomial CUBIC;

    /**
     * A simple quartic function; f(x) = x<sup>4</sup>
     */
    public static final Polynomial QUARTIC;

    /**
     * A simple quintic function; f(x) = x<sup>5</sup>
     */
    public static final Polynomial QUINTIC;

    static {
        double[] constant = {1.0};
        CONSTANT = new Polynomial(constant);

        double[] linear = {0.0, 1.0};
        LINEAR = new Polynomial(linear);

        double[] quadratic = {0.0, 0.0, 1.0};
        QUADRATIC = new Polynomial(quadratic);

        double[] cubic = {0.0, 0.0, 0.0, 1.0};
        CUBIC = new Polynomial(cubic);

        double[] quartic = {0.0, 0.0, 0.0, 0.0, 1.0};
        QUARTIC = new Polynomial(quartic);

        double[] quintic = {0.0, 0.0, 0.0, 0.0, 0.0, 1.0};
        QUINTIC = new Polynomial(quintic);
    }
    /** 
     * The coefficients of the polynomial function 
     * the last element is the coefficient of the highest power,
     * the second last element is the coefficient of the second highest power, 
     * etc., the first element is the constant.
     */
    private final double[] coefficients;

    /**
     * Stores the length of the coefficients array for faster and
     * simpler access
     */
    private final int length;

    private static final double EPSILEON = 0.0000001;
    
    /**
     * Instantiates a Polynomial with the given coefficients.
     * @param coefficients 
     */
    public Polynomial(double[] coefficients) {
        if ((coefficients == null) || (coefficients.length == 0)) {
            this.coefficients = new double[]{0.0};
        } else if (Math.abs(coefficients[coefficients.length - 1]) < EPSILEON) {
            // If one or more of the leading power's coefficients are zero
            // do not include it in our coefficients. This is done by copying
            // the content of the given coefficients up to actualDegree + 1
            int actualDegree = coefficients.length - 2;
            while ((actualDegree > 0)
                    && (Math.abs(coefficients[actualDegree]) < EPSILEON)) {
                actualDegree--;
            }

            this.coefficients = new double[actualDegree + 1];
            System.arraycopy(coefficients, 0, this.coefficients,
                    0, actualDegree + 1);
        } else {
            this.coefficients = coefficients;
        }

        this.length = this.coefficients.length;
    }

    /**
     * Returns the degree of this Polynomial.
     * @return the degree of this Polynomial
     */
    public int getDegrees() {
        return length - 1;
    }

    /**
     * Returns a copy of this Polynomial's coefficients (hence modifying the
     * returned array does not affect this Polynomial's coefficients). 
     * The first element in the array (index 0) is the constant of the 
     * polynomial. The second element (index 1) is the coefficient of the 
     * term with degree 1, the third elment (index 2) is the coefficient
     * of the term with degree 2, etc. For example, if the Polynomial
     * is: f(x) = 5.0 + 2.0x - 9.8x^2, then the double[] array that 
     * would be returned is: [5.0, 2.0, -9.8]. 
     * 
     * @return a double[] array containing the coefficients of the polynomial.
     */
    public double[] getCoefficients() {
        double[] coefficientCopy = new double[length];
        System.arraycopy(this.coefficients, 0, coefficientCopy, 0, length);
        return coefficientCopy;
    }

    /**
     * Returns the sum of the given Polynomial and this Polynomial. 
     * Polynomials are added by adding the coefficients with the same
     * degree together. The given Polynomial and this Polynomial is
     * not changed.
     * 
     * @param anotherPolynomial
     * @return a new Polynomial that is the sum of this Polynomial
     * and the given Polynomial.
     */
    public Polynomial add(Polynomial anotherPolynomial) {

        double[] sum;
        if (length > anotherPolynomial.length) {
            sum = new double[length];
            System.arraycopy(this.coefficients, 0, sum, 0, length);
            for (int i = 0; i < anotherPolynomial.length; i++) {
                sum[i] += anotherPolynomial.coefficients[i];
            }
        } else {
            sum = new double[anotherPolynomial.length];
            System.arraycopy(anotherPolynomial.coefficients, 0,
                    sum, 0, anotherPolynomial.length);
            for (int i = 0; i < length; i++) {
                sum[i] += this.coefficients[i];
            }
        }

        return new Polynomial(sum);
    }

    /**
     * Returns the difference between given Polynomial and this Polynomial 
     * (result = this - anotherPolynomial). Polynomials are subtracted by 
     * subtracting the coefficients with the same degree together. The 
     * given Polynomial and this Polynomial are unchanged.
     * 
     * @param anotherPolynomial
     * @return a new Polynomial that is the sum of this Polynomial
     * and the given Polynomial.
     */
    public Polynomial subtract(Polynomial anotherPolynomial) {

        double[] sum;
        if (length > anotherPolynomial.length) {
            sum = new double[length];
            System.arraycopy(this.coefficients, 0, sum, 0, length);
            for (int i = 0; i < anotherPolynomial.length; i++) {
                sum[i] += anotherPolynomial.coefficients[i];
            }
        } else {
            sum = new double[anotherPolynomial.length];
            System.arraycopy(anotherPolynomial.coefficients, 0,
                    sum, 0, anotherPolynomial.length);
            for (int i = 0; i < length; i++) {
                sum[i] += this.coefficients[i];
            }
        }

        return new Polynomial(sum);
    }
    
    /**
     * Returns a Polynomial which has coefficients that are product of 
     * the coefficients of this Polynomial and the given scalar value. For
     * example if this Polynomial is: f(x) = 3.0 + 7.4x, and the scalar value
     * is: 2.0, then the returned Polynomial is: f(x) = 6.0 + 14.8x.
     *  
     * @param scalar a real number 
     * @return a Polynomial containing the product of this coefficient and
     * the given scalar value
     */
    public Polynomial multiply(double scalar) {
        double[] productCoefficients = new double[length];
        for (int i = 0; i < length; i++) {
            productCoefficients[i] = this.coefficients[i] * scalar;
        }
        return new Polynomial(productCoefficients);
    }

    /**
     * Returns the product of the given Polynomial and this Polynomial. 
     * For example if this Polynomial is: f(x) = 2.0 + 3.0x, and the
     * given Polynomial is: f(x) = 1.0 + 4.0x, then the returned Polynomial
     * will be: f(x) = 2.0 + 7.0x + 12.0x^2.
     * 
     * @param anotherPolynomial
     * @return a new Polynomial that is the product of this Polynomial
     * and the given Polynomial.
     */
    public Polynomial multiply(Polynomial anotherPolynomial) {
        // The resulting polynomial will have degree equal to the sum
        // of this polynomial's degree and the given polynomial's degree
        // The extra +1 is for the constant
        double[] productCoefficients =
                new double[this.getDegrees() + anotherPolynomial.getDegrees()
                + 1];
        for (int i = 0; i < productCoefficients.length; i++) {
            productCoefficients[i] = 0;
        }

        double thisCoefficient;
        for (int i = 0; i < this.length; i++) {
            thisCoefficient = this.coefficients[i];
            for (int j = 0; j < anotherPolynomial.length; j++) {
                productCoefficients[i + j] += thisCoefficient
                        * anotherPolynomial.coefficients[j];
            }
        }
        return new Polynomial(productCoefficients);
    }
        
    /**
     * Returns true if and only if each component of the given 
     * Polynomial has the same coefficient and degree as this Polynomial.
     * Returns false otherwise. 
     * 
     * @param anotherPolynomial the other Polynomial to be compared to this
     * Polynomial.
     * 
     * @return true if this Polynomial is equal to the given Polynomial,
     * false otherwise.
     */
    public boolean equals(Polynomial anotherPolynomial) {
        if (this.length != anotherPolynomial.length) {
            return false;
        }
        for (int i = 0; i < length; i++) {
            if (Math.abs(this.coefficients[i]
                    - anotherPolynomial.coefficients[i]) > EPSILEON) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns true if and only if the given Object is 
     * of type Polynomial and each component of the given Polynomial has 
     * the same coefficient and degree as this Polynomial.
     * Returns false otherwise.
     * 
     * @param anotherObject the other Object to be compared to this Polynomial
     * @return true if the given Object is equal to this Polynomial. False
     * otherwise.
     */
    @Override
    public boolean equals(Object anotherObject) {
        if (anotherObject instanceof Polynomial) {
            return this.equals((Polynomial) anotherObject);
        }
        return false;
    }

    /**
     * Generates the hash code of this Polynomial Object using the hash
     * code of the coefficient array.
     * 
     * @return the hash code of this Polynomial Object.
     */
    @Override
    public int hashCode() {
        int hash = 5;
        hash = 61 * hash + Arrays.hashCode(this.coefficients);
        return hash;
    }

    /**
     * Returns the y-value of this Polynomial at the given 
     * x-value. Note that usually null would be returned if a function 
     * is undefined at the given x-value, however this is not the case
     * for polynomials, since polynomials are defined for all x-values.
     *  
     * @param x
     * @return the y-value of this Polynomial at the given x-value.
     */
    @Override
    public Double evaluate(Double x) {
        double sum = 0;

        for (int i = 0; i < this.coefficients.length; i++) {
            sum += this.coefficients[i] * Math.pow(x, i);
        }

        return sum;
    }

    /**
     * Returns the default GraphingProperties.
     * 
     * @see GraphingProperties
     * @return a GraphingProperties Object used to graph this Polynomial.
     */
    public GraphingProperties getGraphProperties() {
        return new GraphingProperties();
    }

    /**
     * <p> Returns the String representation of this Polynomial in the following
     * form: </p>
     * <p> f(x) = c<sub>0</sub> + c<sub>1</sub>x + c<sub>2</sub>x<sup>2</sup> 
     * + ... + c<sub>n</sub>x<sup>n</sup> </p>
     * 
     * @return the String representation of this Polynomial.
     */
    @Override
    public String toString() {
        String returnStr = "f(x) = " + Double.toString(this.coefficients[0]);
        for (int i = 1; i < length; i++) {
            if (this.coefficients[i] < 0) {
                returnStr += " - " + Math.abs(this.coefficients[i]) + "x^" + i;
            } else {
                returnStr += " + " + this.coefficients[i] + "x^" + i;
            }

        }
        return returnStr;
    }
}
